package com.example.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.example.gulimall.product.service.CategoryBrandRelationService;
import com.example.gulimall.product.vo.Catelog2Vo;
import org.apache.tomcat.jni.Time;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.common.utils.PageUtils;
import com.example.common.utils.Query;

import com.example.gulimall.product.dao.CategoryDao;
import com.example.gulimall.product.entity.CategoryEntity;
import com.example.gulimall.product.service.CategoryService;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {

        //1. 查出所有分类
        List<CategoryEntity> categoryEntities = baseMapper.selectList(null);

        //2.组装父子结构

        //2.1找到根
        List<CategoryEntity> level1Menus = categoryEntities.stream().filter((entity) -> {
            return entity.getParentCid() == 0;
        }).map((menu)->{
            menu.setChildren(getChildrens(menu,categoryEntities));
            return menu;
        }).sorted((menu1,menu2)->{
            return (menu1.getSort() == null ? 0:menu1.getSort())-(menu2.getSort() == null ? 0 :menu2.getSort());
        }).collect(Collectors.toList());



        return level1Menus;
    }

    @Override
    public void removeMneuByIds(List<Long> asList) {
        //TODO 1.检查当前删除的菜单是否被引用

        baseMapper.deleteBatchIds(asList);
    }

    @Override
    public Long[] findCatelogPath(Long catelogPath) {

        List<Long> paths = new ArrayList<>();
        findParentPath(catelogPath,paths);
        Collections.reverse(paths);
//        System.out.println(paths.toString());
        return paths.toArray(new Long[0]);
    }

    //组合操作清除数据
    /*
        1. 删除指定key
        @Caching(evict = {
                @CacheEvict(value = "category",key = "'getLevel1Category'"),
                @CacheEvict(value = "category",key = "'xxxxxxxx'")
        })
        2.删除指定分区
        @CacheEvit(value = "xxxx",allEntite = true)
     */

    // 清楚某一个redis数据
    @CacheEvict(value = "category",key = "'getLevel1Category'")
    @Override
    public void updateDetail(CategoryEntity category) {
        this.updateById(category);
        if(!StringUtils.isEmpty(category.getName())){
            categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
        }
    }
    /*
    Cache需要注明具体到哪个名字的缓存[缓存的分区]
    如果缓存中有，方法不再调用，如果缓存中没有，方法再去调用
    默认行为：
        1. key默认自动生成，包含缓存区名::Simplekey[](自动生成的key)
        2. 缓存的value值，默认使用jdk序列化机制，将序列化后的数据放在redis
        3. 默认时间是-1

        自定义操作：
        1. 设置生成的key key属性指定，接受spel表达式
        2. 指定缓存存活时间 配置文件中指定ttl
        3. 指定序列化方式是json
            默认CacheAutoConfiguation->RedisCacheConfiguration
            需要配置自定义文件 放一个RedisCacheConfiguration的配置文件
            就会自动应用到对应的对象上 配置文件MyCacheConfig

        Spring-Cache的不足：
        1. 读模式
            缓存击穿：大量查询同一个过期的数据。加锁：方法：@Cacheable注解中有sync方法
            缓存雪崩：大量key过期，使用ttl续期进行解决
            缓存穿透：查询一个null数据，cache-null-value=true

        2. 写模式
            1.读写加锁
            2.引入Canal，感知到Mysql更新则更新redis
            3.读多写多还不如去数据库

        总结：常规数据可以使用Spring-Cache的写模式，特殊数据：特殊设计。
     */
    @Cacheable(value = {"Product"}, key = "#root.method.name")
    @Override
    public List<CategoryEntity> getLevel1Category() {

        return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    }


    // 直接调用Redisson
    /*
        缓存一致性：保证缓存和数据库中的数据保持一致
        1、双写模式：同时写数据库和缓存！必须加锁不然中间突然窜出来个人就gg
        2、失效模式：写数据库的同时让缓存失效！还是必须加锁不然会有脏数据
        加的锁必须是读写锁！！！

        解决方案：
        放入缓存的数据不应该是实时性很高！要一直改的！这种不如直接去数据库查！
        不高的可以给它加上过期时间，保证能拿到最新数据即可

        不应该过度设计，增加系统的复杂性
        这里要经常用读写锁！！！！！
     */

    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedis(){

        // 锁的粒度：具体到某个商品
        RLock lock = redisson.getLock("lock");
        Map<String, List<Catelog2Vo>> dataFromdb;
        //无论如何都要释放锁
        try {
            dataFromdb = getCatalogJsonDBNoLock();
        }finally {
            lock.unlock();
        }
        return dataFromdb;
    }

    // 分布式锁：本质是加在redis里的锁
    public Map<String, List<Catelog2Vo>> getCatalogJsonWithRedisLock(){

        //去redis占坑 每个线程应该有自己的标记
        String s = UUID.randomUUID().toString();
        // 300秒很长了！一般没有哪个业务会执行这么久
        // 这里为什么要写在一起？因为防止加锁后再去设置过期时间之间会崩溃！这样直接狗带
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", s, 300, TimeUnit.SECONDS);
        if (lock) {
            Map<String, List<Catelog2Vo>> dataFromdb;
            //无论如何都要释放锁
            try {
                dataFromdb = getCatalogJsonDBNoLock();
            }finally {
                // 这里注意也要原子操作！因为你防止你找到uuid再去删的时候你删不到自己的锁了？？
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1])else return 0 end";
                //删除锁,Lua脚本
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"),s);
            }
            return dataFromdb;
        }else {
            Time.sleep(200);
            // 自选
            return getCatalogJsonWithRedisLock();
        }
    }




    // 出现栈外内存溢出
    // springboot使用lettuce作为操作redis的客户端，它使用netty进行网络通信
    // lettuce的bug导致对外内存溢出
    // 解决方案：
    // 1.升级lettuce客户端 2.使用jedis
    // Redis中容易出现的问题
    /*
        缓存穿透：查询不存在的数据 解决方案：null结果缓存 并加入短暂过期时间
        缓存雪崩：大量数据同时在redis中失效 解决方案：加一个随机时间
        缓存击透：大量服务同时访问某一个失效数据 解决方案：加锁 查数据库前先查redis
     */
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {

        // 1.加入缓存逻辑 寸的对象是json字符串(可以跨语言 跨平台)
        String json = redisTemplate.opsForValue().get("catalogJson");
        if(StringUtils.isEmpty(json)){
            Map<String, List<Catelog2Vo>> db = getCatalogJsonDB();
            return db;
        }

        Map<String, List<Catelog2Vo>> result = JSON.parseObject(json,new TypeReference<Map<String, List<Catelog2Vo>> >(){});
        return result;
    }


    // DB抽取数据
    public Map<String, List<Catelog2Vo>> getCatalogJsonDBNoLock() {

        /*
        本地锁 只要是同一个锁，就可以锁住这个锁的所有线程
         */
            String json = redisTemplate.opsForValue().get("catalogJson");
            if(!StringUtils.isEmpty(json)) {
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(json,new TypeReference<Map<String, List<Catelog2Vo>> >(){});
                return result;
            }
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            // 1. 查询第一级分类
            List<CategoryEntity> level1Category = getParent_cid(selectList,0L);

            Map<String, List<Catelog2Vo>> map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

                // 查询所有二级分类
                List<CategoryEntity> entities = getParent_cid(selectList,v.getCatId());
                List<Catelog2Vo> vos = null;
                if (entities != null) {
                    vos = entities.stream().map(item -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());
                        // 三级
                        List<CategoryEntity> entities1 = getParent_cid(selectList,item.getCatId());
                        if (entities1 != null) {
                            List<Catelog2Vo.Catelog3Vo> catelog3Vos = entities1.stream().map(level3item -> {
                                return new Catelog2Vo.Catelog3Vo(item.getCatId().toString(), level3item.getCatId().toString(), level3item.getName());
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(catelog3Vos);
                        }
                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                return vos;
            }));

            String jsonString = JSON.toJSONString(map);
            redisTemplate.opsForValue().set("catalogJson",jsonString);
            return map;
    }

    // DB抽取数据
    public Map<String, List<Catelog2Vo>> getCatalogJsonDB() {

        /*
        本地锁 只要是同一个锁，就可以锁住这个锁的所有线程
         */
        synchronized (this){
            String json = redisTemplate.opsForValue().get("catalogJson");
            if(!StringUtils.isEmpty(json)) {
                Map<String, List<Catelog2Vo>> result = JSON.parseObject(json,new TypeReference<Map<String, List<Catelog2Vo>> >(){});
                return result;
            }
            List<CategoryEntity> selectList = baseMapper.selectList(null);

            // 1. 查询第一级分类
            List<CategoryEntity> level1Category = getParent_cid(selectList,0L);

            Map<String, List<Catelog2Vo>> map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {

                // 查询所有二级分类
                List<CategoryEntity> entities = getParent_cid(selectList,v.getCatId());
                List<Catelog2Vo> vos = null;
                if (entities != null) {
                    vos = entities.stream().map(item -> {
                        Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, item.getCatId().toString(), item.getName());
                        // 三级
                        List<CategoryEntity> entities1 = getParent_cid(selectList,item.getCatId());
                        if (entities1 != null) {
                            List<Catelog2Vo.Catelog3Vo> catelog3Vos = entities1.stream().map(level3item -> {
                                return new Catelog2Vo.Catelog3Vo(item.getCatId().toString(), level3item.getCatId().toString(), level3item.getName());
                            }).collect(Collectors.toList());
                            catelog2Vo.setCatalog3List(catelog3Vos);
                        }
                        return catelog2Vo;
                    }).collect(Collectors.toList());
                }
                return vos;
            }));

            String jsonString = JSON.toJSONString(map);
            redisTemplate.opsForValue().set("catalogJson",jsonString);
            return map;
        }
    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList, Long parent_cid) {
        return selectList.stream().filter(item->item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
    }

    private void findParentPath(Long catelogPath, List<Long> paths) {
        paths.add(catelogPath);
        CategoryEntity ce = this.getById(catelogPath);
        if(ce.getParentCid() != 0){
            findParentPath(ce.getParentCid(),paths);
        }
    }

    //2.2找到子分类
    private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all){

        List<CategoryEntity> entities = all.stream().filter((entity) -> {
            return entity.getParentCid() == root.getCatId();
        }).map((entity) -> {
            entity.setChildren(getChildrens(entity, all));
            return entity;
        }).sorted((menu1, menu2) -> {
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());

        return entities;
    }
}